#include "QxPLService.h"

#include <QCoreApplication>
#include <QHostInfo>

int const QxPLService::XPL_MINIMUM_PORT_AREA = 49152;
int const QxPLService::XPL_MAXIMUM_PORT_AREA = 65535;
int const QxPLService::XPL_DEFAULT_PORT = 3865;
int const QxPLService::XPL_DEFAULT_CONFIG_HBEAT = 60000; // default 1 minutes
int const QxPLService::XPL_DEFAULT_HBEAT = 5*60000;      // default 5 minutes

/**
   @brief QxPLService::QxPLService
   @param vendor
   @param device
   @param configurable
   @param parent
 */
QxPLService::QxPLService(const QString& vendor, const QString& device, bool configurable, QObject* parent)
    : QObject(parent)
{
    hubDiscovered = false;
    listening = false;
    configurationHeartbeatInterval = XPL_DEFAULT_CONFIG_HBEAT;
    heartbeatInterval = XPL_DEFAULT_HBEAT;
    ipAddress = "";
    port = -1;
    socket = NULL;
    heartbeatTimer = NULL;
    this->configurable = configurable;
    this->configured = false;
    /*
      xPL Core Documentation:
        In the case of a PC-based application, the instance could be the hostname of the
        computer, appended with a unique identifier.
      */
    identifier.setVendorId(vendor);
    identifier.setDeviceID(device);
    identifier.setInstanceId(configurable ? "temporary" : "default");
    qRegisterMetaType<QxPLMessage>();
    connect(this, SIGNAL(messageReceived(QxPLMessage)), this, SLOT(processMessage(QxPLMessage)));
}

/**
   @brief QxPLService::~QxPLService
 */
QxPLService::~QxPLService()
{
    close();
}

/**
   @brief QxPLService::listen
 */
void QxPLService::listen(bool configRequested)
{
    qDebug() << "[xPL] initialize network access...";
    socket = new QUdpSocket();
    ipAddress = retrieveLocalIpAddress();
    if(ipAddress.isEmpty()) {
        qDebug() << "[xPL] ERROR: No network interface found on the host machine";
        return;
    }
    qDebug() << "[xPL] Network interface found: " << ipAddress;
    if(configurable && configRequested) {
        QHostInfo HI = QHostInfo::fromName(ipAddress);
        qDebug() << "[xPL] need configuration: yes";
        identifier.setInstanceId(HI.localHostName().toLower()
                                 + QString::number(QCoreApplication::applicationPid(), 16).toUpper());
        emit identifierChanged();
    }
    else {
        qDebug() << "[xPL] need configuration: no";
    }
    qDebug() << "[xPL] id=" << identifier.toString();
    port = XPL_MINIMUM_PORT_AREA;
    while(!socket->bind(port, QUdpSocket::DontShareAddress)) {
        port++;
        if(port > XPL_MAXIMUM_PORT_AREA) {
            qDebug() << "[xPL] ERROR: No port available";
            return;
        }
    }
    qDebug() << "[xPL] using port: " << port;
    QObject::connect(socket, SIGNAL(readyRead()), this, SLOT(readPendingDatagrams()));
    listening = true;

    heartbeatTimer = new QTimer(this);
    if(configurable && configRequested)
        heartbeatTimer->setInterval(configurationHeartbeatInterval);
    else
        heartbeatTimer->setInterval(heartbeatInterval);
    qDebug() << "[xPL] hbeat every " << heartbeatTimer->interval() << "msec";
    QObject::connect(heartbeatTimer, SIGNAL(timeout()), this, SLOT(heartbeatTimeout()));
    this->heartbeatTimeout();
}

/**
   @brief QxPLService::close
 */
void QxPLService::close()
{
    qDebug() << "[xPL] closing communication...";
    if(socket) {
        if(socket->isOpen())
            socket->close();
        delete socket;
    }
    listening = false;
    qDebug() << "[xPL] communication closed !";
}

/**
   @brief QxPLService::retrieveLocalIpAddress
   @return
 */
QString QxPLService::retrieveLocalIpAddress() const
{
    foreach (const QHostAddress &address, QNetworkInterface::allAddresses()) {
        if (address.protocol() == QAbstractSocket::IPv4Protocol && address != QHostAddress(QHostAddress::LocalHost))
            return address.toString();
    }
    return "";
}

/**
   @brief QxPLService::heartbeatTimeout
 */
void QxPLService::heartbeatTimeout()
{
    heartbeatTimer->stop();
    if(configurable && !configured)
        heartbeatTimer->setInterval(configurationHeartbeatInterval);
    else
        heartbeatTimer->setInterval(heartbeatInterval);
    sendHeartbeat();
    heartbeatTimer->start();
}

/**
   @brief QxPLService::readPendingDatagrams
 */
void QxPLService::readPendingDatagrams()
{
    while(socket->hasPendingDatagrams()) {
        QByteArray datagram;
        datagram.resize(socket->pendingDatagramSize());
        QHostAddress sender;
        quint16 senderPort;
        socket->readDatagram(datagram.data(), datagram.size(), &sender, &senderPort);
        processDatagram(datagram);
    }
}

/**
   @brief QxPLService::processDatagram
   @param datagram
 */
void QxPLService::processDatagram(const QByteArray& datagram)
{
    QString strDatagram = datagram;
    bool ok = false;
    QxPLMessage message = QxPLMessage::createMessage(strDatagram, &ok);
    if(ok) processPendingMessage(message);
    else qDebug() << "[xpl] message skipped";
}

/**
   @brief QxPLService::processMessage
   @param message
 */
void QxPLService::processPendingMessage(const QxPLMessage &message)
{
    /* if the message is a heartbeat and is coming from this xPL service, the hub is discovered ! */
    if(!hubDiscovered && message.getSource() == getIdentifier()
            && (message.getMessageSchema() == "hbeat" || message.getMessageSchema() == "config")
            && message.getMessageClass() == "app"
            && message.getNamedValue("remote-ip") == ipAddress)
    {
        hubDiscovered = true;
        qDebug() << "[xpl] hub discovered !";
        qDebug() << "[xpl] heartbeat interval is now set to " << heartbeatInterval;
        qDebug() << "[xpl] communication is now available !";
    }

    if(!matchAddress(message.getTarget())) {
        return;
    }
    if(!applyFilters(message)) {
        return;
    }

    if(message.getType() == QxPLMessage::COMMAND
            && message.getMessageSchema() == "config"
            && message.getMessageClass() == "response")
        processConfigurationMessage(message);

    if(message.getType() == QxPLMessage::COMMAND
            && message.getMessageSchema() == "config"
            && message.getMessageClass() == "list"
            && message.contains("command")
            && message.getNamedValue("command") == "request")
        sendConfigurationCapabilities();

    if(message.getType() == QxPLMessage::COMMAND
            && message.getMessageSchema() == "config"
            && message.getMessageClass() == "current"
            && message.contains("command")
            && message.getNamedValue("command") == "request")
        sendCurrentConfiguration();

    emit messageReceived(message);
    qDebug() << "[xpl] emit messageReceived(message);";
}

/**
   @brief QxPLService::addFilter
   @param str_filter
 */
void QxPLService::addFilter(const QString &str_filter)
{
    QxPLFilter filter;
    if(filter.init(str_filter))
        filters().append(filter);
}

/**
   @brief QxPLService::sendCurrentConfiguration
 */
void QxPLService::sendCurrentConfiguration()
{
    QxPLMessage message(QxPLMessage::STATUS, "config", "current");
    message.setHop(1);
    message.setSource(getIdentifier());
    message.setTarget(QxPLIdentifier::broadcast());
    message.setNamedValue("newconf", getIdentifier().getInstanceId());
    message.setNamedValue("interval", QString::number(heartbeatInterval / 1000 / 60));
    message.setNamedValue("group", ""); // process not supported yet ! (TODO)
    for(int i = 0; i < filters().count(); i++) {
        message.setNamedValue("filter", filters().at(i).toString(), i);
    }
    // warning: list of config values (i.e. foo[10]) not supported yet ! (TODO)
    for(int i = 0; i < configValueList().count(); i++) {
        message.setNamedValue(configValueList().at(i).getKey(), configValueList().at(i).getValue());
    }
    sendMessage(message);
}

/**
   @brief QxPLService::matchAddress
   @param target
   @return
 */
bool QxPLService::matchAddress(const QxPLIdentifier &target) const
{
    if(target.isBroadcast()) return true;
    if(target.getVendorId() == identifier.getVendorId()) {
        if(target.getDeviceId() == "*") return true;
        if(target.getDeviceId() == identifier.getDeviceId()) {
            if(target.getInstanceId() == "*") return true;
            if(target.getInstanceId() == identifier.getInstanceId())
                return true;
        }
    }
    return false;
}

/**
   @brief QxPLService::applyFilters
   @param message
   @return
 */
bool QxPLService::applyFilters(const QxPLMessage& message) const
{
    if(m_filters.isEmpty())
        return true;
    for(int i = 0; i < m_filters.count(); i++) {
        if(m_filters.at(i).match(message))
            return true;
    }
    return false;
}

/**
   @brief QxPLService::sendMessage
   @param message
 */
void QxPLService::sendMessage(QxPLMessage message)
{
    if(this->isListening()) {
        QByteArray datagram = message.toString().toLocal8Bit();
        socket->writeDatagram(datagram.data(), datagram.size(),
                                         QHostAddress::Broadcast, XPL_DEFAULT_PORT);
    }
}

/**
   @brief QxPLService::sendHeartbeat
 */
void QxPLService::sendHeartbeat()
{
    QxPLMessage message;
    message.setType(QxPLMessage::STATUS);
    message.setHop(1);
    message.setSource(this->getIdentifier());
    message.setTarget(QxPLIdentifier::broadcast());
    if(configurable && !configured)
        message.setMessageSchema("config");
    else
        message.setMessageSchema("hbeat");
    message.setMessageClass("app");
    message.append("interval", QString::number(heartbeatTimer->interval() / 1000 / 60));
    message.append("port", QString::number(port));
    message.append("remote-ip", this->ipAddress);
    if(!version.isEmpty())
        message.append("version", version);
    sendMessage(message);
}

/**
   @brief QxPLService::getIdentifier
   @return
 */
const QxPLIdentifier& QxPLService::getIdentifier() const
{
    return identifier;
}

/**
   @brief QxPLService::getIdentifier
   @return
 */
QxPLIdentifier& QxPLService::getIdentifier()
{
    return identifier;
}

/**
   @brief QxPLService::setIdentifier
   @param identifier
 */
void QxPLService::setIdentifier(const QxPLIdentifier& identifier)
{
    this->identifier = identifier;
    emit this->identifierChanged();
}

/**
   @brief QxPLService::getIpAddress
   @return
 */
const QString& QxPLService::getIpAddress() const
{
    return ipAddress;
}

/**
   @brief QxPLService::getPort
   @return
 */
int QxPLService::getPort() const
{
    return port;
}

/**
   @brief QxPLService::getHeartbeatInterval
   @return
 */
int QxPLService::getHeartbeatInterval() const
{
    return heartbeatInterval;
}

/**
   @brief QxPLService::isHubDicovered
   @return
 */
bool QxPLService::isHubDicovered() const
{
    return hubDiscovered;
}

/*
   @brief QxPLService::isFixedInstanceId
   @return
 */
/*bool QxPLService::isFixedInstanceId() const
{
    return fixedInstanceId;
}*/

/**
   @brief QxPLService::filters
   @return
 */
QList<QxPLFilter>& QxPLService::filters()
{
    return m_filters;
}

/**
   @brief QxPLService::filters
   @return
 */
const QList<QxPLFilter>& QxPLService::filters() const
{
    return m_filters;
}

/**
   @brief QxPLService::isListening
   @return
 */
bool QxPLService::isListening() const
{
    return listening;
}

/*
   @brief QxPLService::setFixedInstanceId
   @param b
 */
/*void QxPLService::setFixedInstanceId(bool b)
{
    this->fixedInstanceId = b;
}*/

/**
   @brief QxPLService::setHeartbeatInterval
   @param interval
 */
void QxPLService::setHeartbeatInterval(int interval)
{
    this->heartbeatInterval = interval;
}

/**
   @brief QxPLService::xPLMessageProcess
 */
void QxPLService::processMessage(QxPLMessage message)
{
    Q_UNUSED(message)
}

/**
   @brief QxPLService::xPL_config_response_cmd
   @param message
 */
bool QxPLService::processConfigurationMessage(const QxPLMessage &message)
{
    if(!isConfigurable()) {
        qDebug() << "[xPL] Device is not configurable !";
        return false;
    }
    /*
        config.response
        {
        newconf=[new instance id]
        [interval=[value]]
        [group=[value]]
        [filter=[value]]
        [developer defined parameters...=[value]]
        ...
        }
      */
    bool conversionOK = false;
    int hbInterval = message.getNamedValue("interval").toInt(&conversionOK);
    if(!conversionOK) {
        qDebug() << "[xPL] message skipped due to unable to convert interval value into integer";
        return false;
    }

    QString reconf = message.getNamedValue("newconf");
    if(reconf.isEmpty()) {
        qDebug() << "[xPL] message skipped due to no newconf specified.";
        return false;
    }

    int group_count = message.count("group");
    if(group_count > 0) {
        groups.clear();
        QString strMessage = "[xPL] group list updated : ";
        for(int i = 0; i < group_count; i++) {
            QString str_group = message.getNamedValue("group", i);
            if(str_group.isEmpty()) continue;
            groups << str_group;
            if(i > 0) strMessage += ", ";
            strMessage += str_group;
        }
        qDebug() << strMessage;
    }

    int filter_count = message.count("filter");
    if(filter_count > 0)  {
        m_filters.clear();
        QString str_message = "[xPL] new filters configured : ";
        for(int i = 0; i < filter_count; i++) {
            QxPLFilter filter;
            QString tmpfilter = message.getNamedValue("filter", i);
            if(tmpfilter.isEmpty() || !filter.init(tmpfilter))
                continue;
            str_message += "\n- "+tmpfilter;
            m_filters.append(filter);
        }
        qDebug() << str_message;
    }


    //On recopie la liste
    QList<QxPLConfigValue> cpy_config_list = this->m_configValueList;
    for(int i = 0; i < cpy_config_list.count(); i++) {
        QString key = cpy_config_list.at(i).getKey();
        qDebug() << "[xPL] setup " << key << " parameter...";
        if(cpy_config_list.at(i).getType() == QxPLConfigValue::CONFIG) {
            //this type of value is required and can only be configured
            //at device startup and it cannot be altered later
            if(cpy_config_list.at(i).getValue().isEmpty()) {
                if(message.count(key) <= 0) {
                    qDebug() << "[xPL] ERROR: config command skipped : " << key << "=config is expected";
                    return false;
                }
                cpy_config_list[i].setValue(message.getNamedValue(key));
            }
            else {
                if(message.count(key) > 0) {
                    qDebug() << "[xPL] WARNING: value skipped " << key << "=config wasn't expected here, the device is already configure !";
                    continue;
                }
            }

        }
        else if(cpy_config_list.at(i).getType() == QxPLConfigValue::RECONF) {
            //this type of value is required and can be re-configured at any time
            if(message.count(key) <= 0) {
                qDebug() << "[xPL] ERROR: config command skipped : " << key << "=config is expected";
                return false;
            }
            cpy_config_list[i].setValue(message.getNamedValue(key));
        }
        else if(cpy_config_list.at(i).getType() == QxPLConfigValue::OPTION) {
            //optional field that may be omitted when responding
            if(!message.getNamedValue(key).isEmpty()) {
                cpy_config_list[i].setValue(message.getNamedValue(key));
            }
        }
    }
    this->m_configValueList = cpy_config_list;
    qDebug() << "[xPL] setup new heartbeat interval to " << hbInterval << " minutes";
    setHeartbeatInterval(hbInterval * 1000 * 60);

    identifier.setInstanceId(reconf);
    emit identifierChanged();
    qDebug() << "[xPL] Configuration done. New instance id is " << identifier.getInstanceId();
    emit configValuesChanged();
    this->setConfigured(true);

    //send immediatly a hbeat with the reconfigured xpl identifier
    // and reset the hbeat timer
    heartbeatTimeout();
    return true;
}

/**
   @brief QxPLService::sendConfigurationCapabilities
 */
void QxPLService::sendConfigurationCapabilities()
{
    //TODO
    qDebug() << "[xPL] capabilities requested";
    qDebug() << "[xPL] sending capabilities...";
    QxPLMessage message(QxPLMessage::STATUS, "config", "list");
    message.setHop(1);
    message.setTarget(QxPLIdentifier::broadcast());
    message.setSource(getIdentifier());
    message.append("reconf", "newconf");
    message.append("option", "interval");
    message.append("option", "group[16]");
    message.append("option", "filter[16]");
    for(int i = 0; i < configValueList().count(); i++) {
        message.append(configStringType(configValueList().at(i).getType()),
                       configValueList().at(i).getKey());
    }
    sendMessage(message);
}

QString QxPLService::configStringType(QxPLConfigValue::QxPLValueType type)
{
    switch(type) {
    case QxPLConfigValue::CONFIG: return "config";
    case QxPLConfigValue::OPTION: return "option";
    case QxPLConfigValue::RECONF: return "reconf";
    }
    return "";
}

/**
   @brief QxPLService::configValueList
   @return
 */
QList<QxPLConfigValue>& QxPLService::configValueList()
{
    return m_configValueList;
}

/**
   @brief QxPLService::configValueList
   @return
 */
const QList<QxPLConfigValue>& QxPLService::configValueList() const
{
    return m_configValueList;
}

/**
   @brief QxPLService::addConfigValue
   @param key
   @param defaultValue
 */
void QxPLService::addConfigValue(const QString &key,
                                 QxPLConfigValue::QxPLValueType type,
                                 const QString &defaultValue)
{
    m_configValueList.append(QxPLConfigValue(key, defaultValue, type));
}

/**
   @brief QxPLService::removeConfigValue
   @param key
 */
void QxPLService::removeConfigValue(const QString &key)
{
    for(int i = 0; i < m_configValueList.count(); i++) {
        if(m_configValueList.at(i).getKey() == key) {
            m_configValueList.removeAt(i);
        }
    }
}

/**
   @brief QxPLService::configValue
   @param key
   @param defaultValue
   @return
 */
const QString& QxPLService::configValue(const QString &key, const QString &defaultValue) const
{
    for(int i = 0; i < m_configValueList.count(); i++) {
        if(m_configValueList.at(i).getKey() == key) {
            return m_configValueList.at(i).getValue();
        }
    }
    return defaultValue;
}

/**
   @brief QxPLService::isConfigured
   @return
 */
bool QxPLService::isConfigured() const
{
    return configured;
}

/**
   @brief QxPLService::setConfigure
   @param configured
 */
void QxPLService::setConfigured(bool configured)
{
    this->configured = configured;
}

/**
   @brief QxPLService::isConfigurable
   @return
 */
bool QxPLService::isConfigurable() const
{
    return configurable;
}

/**
   @brief QxPLService::setConfigurable
   @param configurable
 */
void QxPLService::setConfigurable(bool configurable)
{
    this->configurable = configurable;
}

/**
   @brief QxPLService::getVersion
   @return
 */
const QString& QxPLService::getVersion() const
{
    return version;
}

/**
   @brief QxPLService::setVersion
   @param version
 */
void QxPLService::setVersion(const QString &version)
{
    this->version = version;
}
